/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.utils;

import cn.easyplatform.lang.Files;
import cn.easyplatform.lang.Lang;
import cn.easyplatform.utils.image.AnimatedGifEncoder;

import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.io.*;
import java.net.URL;
import java.util.Iterator;

/**
 * 对图像操作的简化 API
 * 
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a>
 */
public class Images {
	/**
	 * 对一个图像进行旋转
	 * 
	 * @param srcIm
	 *            原图像文件
	 * @param taIm
	 *            转换后的图像文件
	 * @param degree
	 *            旋转角度, 90 为顺时针九十度， -90 为逆时针九十度
	 * @return 旋转后得图像对象
	 */
	public static BufferedImage rotate(Object srcIm, File taIm, int degree) {
		BufferedImage im = Images.read(srcIm);
		BufferedImage im2 = Images.rotate(im, degree);
		Images.write(im2, taIm);
		return im2;
	}

	/**
	 * 对一个图像进行旋转
	 * 
	 * @param srcPath
	 *            原图像文件路径
	 * @param taPath
	 *            转换后的图像文件路径
	 * @param degree
	 *            旋转角度, 90 为顺时针九十度， -90 为逆时针九十度
	 * @return 旋转后得图像对象
	 */
	public static BufferedImage rotate(String srcPath, String taPath, int degree)
			throws IOException {
		File srcIm = Files.findFile(srcPath);
		if (null == srcIm)
			throw Lang.makeThrow("Fail to find image file '%s'!", srcPath);

		File taIm = Files.createFileIfNoExists(taPath);
		return rotate(srcIm, taIm, degree);
	}

	/**
	 * 对一个图像进行旋转
	 * 
	 * @param image
	 *            图像
	 * @param degree
	 *            旋转角度, 90 为顺时针九十度， -90 为逆时针九十度
	 * @return 旋转后得图像对象
	 */
	public static BufferedImage rotate(BufferedImage image, int degree) {
		int iw = image.getWidth();// 原始图象的宽度
		int ih = image.getHeight();// 原始图象的高度
		int w = 0;
		int h = 0;
		int x = 0;
		int y = 0;
		degree = degree % 360;
		if (degree < 0)
			degree = 360 + degree;// 将角度转换到0-360度之间
		double ang = degree * 0.0174532925;// 将角度转为弧度

		/**
		 * 确定旋转后的图象的高度和宽度
		 */

		if (degree == 180 || degree == 0 || degree == 360) {
			w = iw;
			h = ih;
		} else if (degree == 90 || degree == 270) {
			w = ih;
			h = iw;
		} else {
			int d = iw + ih;
			w = (int) (d * Math.abs(Math.cos(ang)));
			h = (int) (d * Math.abs(Math.sin(ang)));
		}

		x = (w / 2) - (iw / 2);// 确定原点坐标
		y = (h / 2) - (ih / 2);
		BufferedImage rotatedImage = new BufferedImage(w, h, image.getType());
		Graphics gs = rotatedImage.getGraphics();
		gs.fillRect(0, 0, w, h);// 以给定颜色绘制旋转后图片的背景
		AffineTransform at = new AffineTransform();
		at.rotate(ang, w / 2, h / 2);// 旋转图象
		at.translate(x, y);
		AffineTransformOp op = new AffineTransformOp(at,
				AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
		op.filter(image, rotatedImage);
		image = rotatedImage;
		return image;
	}

	/**
	 * 自动等比缩放一个图片，并将其保存成目标图像文件<br />
	 * 多余的部分，用给定背景颜色补上<br />
	 * 如果参数中的宽度或高度为<b>-1</b>的话，着按照指定的高度或宽度对原图等比例缩放图片，不添加背景颜色
	 * <p>
	 * 图片格式支持 png | gif | jpg | bmp | wbmp
	 * 
	 * @param srcIm
	 *            源图像文件对象
	 * @param taIm
	 *            目标图像文件对象
	 * @param w
	 *            宽度
	 * @param h
	 *            高度
	 * @param bgColor
	 *            背景颜色
	 * 
	 * @return 被转换前的图像对象
	 * 
	 * @throws IOException
	 *             当读写文件失败时抛出
	 */
	public static BufferedImage zoomScale(Object srcIm, File taIm, int w,
			int h, Color bgColor) throws IOException {
		BufferedImage old = read(srcIm);
		BufferedImage im = zoomScale(old, w, h, bgColor);
		write(im, taIm);
		return old;
	}

	/**
	 * 自动等比缩放一个图片，并将其保存成目标图像文件<br />
	 * 多余的部分，用给定背景颜色补上<br />
	 * 如果参数中的宽度或高度为<b>-1</b>的话，着按照指定的高度或宽度对原图等比例缩放图片，不添加背景颜色
	 * <p>
	 * 图片格式支持 png | gif | jpg | bmp | wbmp
	 * 
	 * @param srcPath
	 *            源图像路径
	 * @param taPath
	 *            目标图像路径，如果不存在，则创建
	 * @param w
	 *            宽度
	 * @param h
	 *            高度
	 * @param bgColor
	 *            背景颜色
	 * 
	 * @return 被转换前的图像对象
	 * 
	 * @throws IOException
	 *             当读写文件失败时抛出
	 */
	public static BufferedImage zoomScale(String srcPath, String taPath, int w,
			int h, Color bgColor) throws IOException {
		File srcIm = Files.findFile(srcPath);
		if (null == srcIm)
			throw Lang.makeThrow("Fail to find image file '%s'!", srcPath);

		File taIm = Files.createFileIfNoExists(taPath);
		return zoomScale(srcIm, taIm, w, h, bgColor);
	}

	/**
	 * 自动等比缩放一个图片，多余的部分，用给定背景颜色补上<br />
	 * 如果参数中的宽度或高度为<b>-1</b>的话，着按照指定的高度或宽度对原图等比例缩放图片，不添加背景颜色
	 * 
	 * @param im
	 *            图像对象
	 * @param w
	 *            宽度
	 * @param h
	 *            高度
	 * @param bgColor
	 *            背景颜色
	 * 
	 * @return 被转换后的图像
	 */
	public static BufferedImage zoomScale(BufferedImage im, int w, int h,
			Color bgColor) {
		if (w == -1 || h == -1) {
			return zoomScale(im, w, h);
		}

		// 检查背景颜色
		bgColor = null == bgColor ? Color.black : bgColor;
		// 获得尺寸
		int oW = im.getWidth();
		int oH = im.getHeight();
		float oR = (float) oW / (float) oH;
		float nR = (float) w / (float) h;

		int nW, nH, x, y;
		/*
		 * 缩放
		 */
		// 原图太宽，计算当原图与画布同高时，原图的等比宽度
		if (oR > nR) {
			nW = w;
			nH = (int) (((float) w) / oR);
			x = 0;
			y = (h - nH) / 2;
		}
		// 原图太长
		else if (oR < nR) {
			nH = h;
			nW = (int) (((float) h) * oR);
			x = (w - nW) / 2;
			y = 0;
		}
		// 比例相同
		else {
			nW = w;
			nH = h;
			x = 0;
			y = 0;
		}

		// 创建图像
		BufferedImage re = new BufferedImage(w, h, ColorSpace.TYPE_RGB);
		// 得到一个绘制接口
		Graphics gc = re.getGraphics();
		gc.setColor(bgColor);
		gc.fillRect(0, 0, w, h);
		gc.drawImage(im, x, y, nW, nH, bgColor, null);
		// 返回
		return re;
	}

	/**
	 * 自动等比缩放一个图片
	 * 
	 * @param im
	 *            图像对象
	 * @param w
	 *            宽度
	 * @param h
	 *            高度
	 * 
	 * @return 被转换后的图像
	 */
	public static BufferedImage zoomScale(BufferedImage im, int w, int h) {
		// 获得尺寸
		int oW = im.getWidth();
		int oH = im.getHeight();

		int nW = w, nH = h;

		/*
		 * 缩放
		 */
		// 未指定图像高度，根据原图尺寸计算出高度
		if (h == -1) {
			nH = (int) ((float) w / oW * oH);
		}
		// 未指定图像宽度，根据原图尺寸计算出宽度
		else if (w == -1) {
			nW = (int) ((float) h / oH * oW);
		}

		// 创建图像
		BufferedImage re = new BufferedImage(nW, nH, ColorSpace.TYPE_RGB);
		re.getGraphics().drawImage(im, 0, 0, nW, nH, null);
		// 返回
		return re;
	}

	/**
	 * 自动缩放剪切一个图片，令其符合给定的尺寸，并将其保存成目标图像文件
	 * <p>
	 * 图片格式支持 png | gif | jpg | bmp | wbmp
	 * 
	 * @param srcIm
	 *            源图像文件对象
	 * @param taIm
	 *            目标图像文件对象
	 * @param w
	 *            宽度
	 * @param h
	 *            高度
	 * @return 被转换前的图像对象
	 * 
	 * @throws IOException
	 *             当读写文件失败时抛出
	 */
	public static BufferedImage clipScale(Object srcIm, File taIm, int w, int h)
			throws IOException {
		BufferedImage old = read(srcIm);
		BufferedImage im = Images.clipScale(old, w, h);
		write(im, taIm);
		return old;
	}

	/**
	 * 自动缩放剪切一个图片，令其符合给定的尺寸，并将其保存到目标图像路径
	 * <p>
	 * 图片格式支持 png | gif | jpg | bmp | wbmp
	 * 
	 * @param srcPath
	 *            源图像路径
	 * @param taPath
	 *            目标图像路径，如果不存在，则创建
	 * @param w
	 *            宽度
	 * @param h
	 *            高度
	 * 
	 * @return 被转换前的图像对象
	 * 
	 * @throws IOException
	 *             当读写文件失败时抛出
	 */
	public static BufferedImage clipScale(String srcPath, String taPath, int w,
			int h) throws IOException {
		File srcIm = Files.findFile(srcPath);
		if (null == srcIm)
			throw Lang.makeThrow("Fail to find image file '%s'!", srcPath);

		File taIm = Files.createFileIfNoExists(taPath);
		return clipScale(srcIm, taIm, w, h);
	}

	/**
	 * 根据给定的起始坐标点与结束坐标点来剪切一个图片，令其符合给定的尺寸，并将其保存成目标图像文件
	 * <p>
	 * 图片格式支持 png | gif | jpg | bmp | wbmp
	 * 
	 * @param srcIm
	 *            源图像文件对象
	 * @param taIm
	 *            目标图像文件对象
	 * @param startPoint
	 *            起始坐标点，其值[x, y]为相对原图片左上角的坐标
	 * @param endPoint
	 *            结束坐标点，其值[x, y]为相对原图片左上角的坐标
	 * @return 被转换前的图像对象
	 * 
	 * @throws IOException
	 *             当读写文件失败时抛出
	 */
	public static BufferedImage clipScale(Object srcIm, File taIm,
			int[] startPoint, int[] endPoint) throws IOException {
		// 计算给定坐标后的图片的尺寸
		int width = endPoint[0] - startPoint[0];
		int height = endPoint[1] - startPoint[1];

		BufferedImage old = read(srcIm);
		BufferedImage im = Images.clipScale(
				old.getSubimage(startPoint[0], startPoint[1], width, height),
				width, height);

		write(im, taIm);
		return old;
	}

	/**
	 * 根据给定的起始坐标点与结束坐标点来剪切一个图片，令其符合给定的尺寸，并将其保存成目标图像文件
	 * <p>
	 * 图片格式支持 png | gif | jpg | bmp | wbmp
	 * 
	 * @param srcIm
	 *            源图像文件对象
	 * @param taIm
	 *            目标图像文件对象
	 * @param startPoint
	 *            起始坐标点，其值[x, y]为相对原图片左上角的坐标
	 * @param endPoint
	 *            结束坐标点，其值[x, y]为相对原图片左上角的坐标
	 * @return 被转换前的图像对象
	 * 
	 * @throws IOException
	 *             当读写文件失败时抛出
	 */
	public static BufferedImage clipScale(String srcPath, String taPath,
			int[] startPoint, int[] endPoint) throws IOException {
		File srcIm = Files.findFile(srcPath);
		if (null == srcIm)
			throw Lang.makeThrow("Fail to find image file '%s'!", srcPath);

		File taIm = Files.createFileIfNoExists(taPath);
		return clipScale(srcIm, taIm, startPoint, endPoint);
	}

	/**
	 * 自动缩放剪切一个图片，令其符合给定的尺寸
	 * <p>
	 * 如果图片太大，则将其缩小，如果图片太小，则将其放大，多余的部分被裁减
	 * 
	 * @param im
	 *            图像对象
	 * @param w
	 *            宽度
	 * @param h
	 *            高度
	 * @return 被转换后的图像
	 */
	public static BufferedImage clipScale(BufferedImage im, int w, int h) {
		// 获得尺寸
		int oW = im.getWidth();
		int oH = im.getHeight();
		float oR = (float) oW / (float) oH;
		float nR = (float) w / (float) h;

		int nW, nH, x, y;
		/*
		 * 裁减
		 */
		// 原图太宽，计算当原图与画布同高时，原图的等比宽度
		if (oR > nR) {
			nW = (h * oW) / oH;
			nH = h;
			x = (w - nW) / 2;
			y = 0;
		}
		// 原图太长
		else if (oR < nR) {
			nW = w;
			nH = (w * oH) / oW;
			x = 0;
			y = (h - nH) / 2;
		}
		// 比例相同
		else {
			nW = w;
			nH = h;
			x = 0;
			y = 0;
		}
		// 创建图像
		BufferedImage re = new BufferedImage(w, h, ColorSpace.TYPE_RGB);
		re.getGraphics().drawImage(im, x, y, nW, nH, Color.black, null);
		// 返回
		return re;
	}

	/**
	 * 将一个图片文件读入内存
	 * 
	 * @param img
	 *            图片文件
	 * @return 图片对象
	 */
	public static BufferedImage read(Object img) {
		try {
			if (img instanceof File)
				return ImageIO.read((File) img);
			else if (img instanceof URL)
				img = ((URL) img).openStream();
			if (img instanceof InputStream) {
				File tmp = File.createTempFile("easyplatform_img", ".jpg");
				Files.write(tmp, (InputStream) img);
				try {
					return read(tmp);
				} finally {
					tmp.delete();
				}
			}
			throw Lang.makeThrow("Unkown img info!! --> " + img);
		} catch (IOException e) {
			try {
				InputStream in = null;
				if (img instanceof File)
					in = new FileInputStream((File) img);
				else if (img instanceof URL)
					in = ((URL) img).openStream();
				else if (img instanceof InputStream)
					in = (InputStream) img;
				if (in != null)
					return readJpeg(in);
			} catch (IOException e2) {
				e2.fillInStackTrace();
			}
			return null;
			// throw Lang.wrapThrow(e);
		}
	}

	/**
	 * 将内存中一个图片写入目标文件
	 * 
	 * @param im
	 *            图片对象
	 * @param targetFile
	 *            目标文件，根据其后缀，来决定写入何种图片格式
	 */
	public static void write(RenderedImage im, File targetFile) {
		try {
			ImageIO.write(im, Files.getSuffixName(targetFile), targetFile);
		} catch (IOException e) {
			throw Lang.wrapThrow(e);
		}
	}

	/**
	 * 写入一个 JPG 图像
	 * 
	 * @param im
	 *            图像对象
	 * @param targetJpg
	 *            目标输出 JPG 图像文件
	 * @param quality
	 *            质量 0.1f ~ 1.0f
	 */
	public static void writeJpeg(RenderedImage im, Object targetJpg,
			float quality) {
		try {
			ImageWriter writer = ImageIO.getImageWritersBySuffix("jpg").next();
			ImageWriteParam param = writer.getDefaultWriteParam();
			param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
			param.setCompressionQuality(quality);
			ImageOutputStream os = ImageIO.createImageOutputStream(targetJpg);
			writer.setOutput(os);
			writer.write((IIOMetadata) null, new IIOImage(im, null, null),
					param);
		} catch (IOException e) {
			throw Lang.wrapThrow(e);
		}
	}

	/**
	 * 尝试读取JPEG文件的高级方法,可读取32位的jpeg文件
	 * <p/>
	 * 来自:
	 * http://stackoverflow.com/questions/2408613/problem-reading-jpeg-image-
	 * using-imageio-readfile-file
	 * 
	 * */
	private static BufferedImage readJpeg(InputStream in) throws IOException {
		Iterator<ImageReader> readers = ImageIO
				.getImageReadersByFormatName("JPEG");
		ImageReader reader = null;
		while (readers.hasNext()) {
			reader = (ImageReader) readers.next();
			if (reader.canReadRaster()) {
				break;
			}
		}
		ImageInputStream input = ImageIO.createImageInputStream(in);
		reader.setInput(input);
		// Read the image raster
		Raster raster = reader.readRaster(0, null);
		BufferedImage image = createJPEG4(raster);
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		writeJpeg(image, out, 1);
		out.flush();
		BufferedImage img = read(new ByteArrayInputStream(out.toByteArray()));
		return img;
	}

	/**
	 * Java's ImageIO can't process 4-component images and Java2D can't apply
	 * AffineTransformOp either, so convert raster data to RGB. Technique due to
	 * MArk Stephens. Free for any use.
	 */
	private static BufferedImage createJPEG4(Raster raster) {
		int w = raster.getWidth();
		int h = raster.getHeight();
		byte[] rgb = new byte[w * h * 3];

		float[] Y = raster.getSamples(0, 0, w, h, 0, (float[]) null);
		float[] Cb = raster.getSamples(0, 0, w, h, 1, (float[]) null);
		float[] Cr = raster.getSamples(0, 0, w, h, 2, (float[]) null);
		float[] K = raster.getSamples(0, 0, w, h, 3, (float[]) null);

		for (int i = 0, imax = Y.length, base = 0; i < imax; i++, base += 3) {
			float k = 220 - K[i], y = 255 - Y[i], cb = 255 - Cb[i], cr = 255 - Cr[i];

			double val = y + 1.402 * (cr - 128) - k;
			val = (val - 128) * .65f + 128;
			rgb[base] = val < 0.0 ? (byte) 0 : val > 255.0 ? (byte) 0xff
					: (byte) (val + 0.5);

			val = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128) - k;
			val = (val - 128) * .65f + 128;
			rgb[base + 1] = val < 0.0 ? (byte) 0 : val > 255.0 ? (byte) 0xff
					: (byte) (val + 0.5);

			val = y + 1.772 * (cb - 128) - k;
			val = (val - 128) * .65f + 128;
			rgb[base + 2] = val < 0.0 ? (byte) 0 : val > 255.0 ? (byte) 0xff
					: (byte) (val + 0.5);
		}

		raster = Raster.createInterleavedRaster(new DataBufferByte(rgb,
				rgb.length), w, h, w * 3, 3, new int[] { 0, 1, 2 }, null);

		ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
		ColorModel cm = new ComponentColorModel(cs, false, true,
				Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
		return new BufferedImage(cm, (WritableRaster) raster, true, null);
	}

	/**
	 * 根据一堆图片生成一个gif图片
	 * 
	 * @param targetFile
	 *            目标输出文件
	 * @param frameFiles
	 *            组成动画的文件
	 * @param delay
	 *            帧间隔
	 * @return 是否生成成功
	 */
	public static boolean writeGif(String targetFile, String[] frameFiles,
			int delay) {
		try {
			AnimatedGifEncoder e = new AnimatedGifEncoder();
			e.setRepeat(0);
			e.start(targetFile);
			for (String f : frameFiles) {
				e.setDelay(delay);
				e.addFrame(ImageIO.read(new File(f)));
			}
			return e.finish();
		} catch (Exception e) {
			return false;
		}
	}

}
